Skip to content

feat(group-chat): 建立 v2 actor/access 与运行时撤权基础 - #2233

Open
hanzckernel wants to merge 3 commits into
EKKOLearnAI:mainfrom
hanzckernel:fix/group-chat-access-invite-leave
Open

feat(group-chat): 建立 v2 actor/access 与运行时撤权基础#2233
hanzckernel wants to merge 3 commits into
EKKOLearnAI:mainfrom
hanzckernel:fix/group-chat-access-invite-leave

Conversation

@hanzckernel

@hanzckernel hanzckernel commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

背景

这个 PR 不是单纯的邀请码、退房和复制按钮改动,而是 Group Chat v2 的第一层安全基础:把房间 actor、capability、邀请准入、REST / Socket.IO 授权和 Agent 运行时撤权统一到一条可持久化、可迁移、可复查的授权链上。

此前房主、成员行、账号可用 Profile、房间 Agent Profile 和 Socket 在线状态分散参与权限判断;请求开始时取得的身份也可能跨越异步操作继续使用。结果是房间读取、实时投递、审批、Agent Bridge 和上下文压缩没有共享同一条权威边界,尤其可能把“账号能使用某个 Agent Profile”误当成“账号能进入包含该 Profile 的房间”。

本 PR 统一以下不变量:

每次房间读取、写入、实时事件投递、Agent 调用、审批响应和房间上下文处理,都必须在实际使用点依据持久化 actor、capability 与 revision lease 重新授权;权限一旦变化,旧请求、旧 Socket、旧 Bridge session 和在途运行必须 fail closed,不能继续读取、投递或提交结果。

改动

1. 持久化 actor、capability 与访问策略

  • 新增房间级 actor / capability 存储,将 authenticated_humanlocalagentsystem 统一为稳定 actor 身份。
  • 建立有限 capability 集:room.readroom.writeroom.typeroom.manageagent.invokeapproval.respond;REST、Socket.IO、Agent 与审批运行时共享同一套 typed policy 结果。
  • 将房主、显式成员、actor grant、Profile entitlement 和运行时在线状态保持为不同权威来源;删除“账号拥有与房间 Agent 同名的 Profile 即自动获得房间访问权”的路径。
  • GET/list 路径不创建权威身份;空 grant fail closed。普通重连、身份刷新和邀请码重入保留已有 grant,不把成员身份静默升级成管理权限。
  • Agent 或账号被移除时保留 tombstone 与 revision,先撤销 actor、membership 和 ownership,再中断运行时,避免旧 actor、旧 session 或迟到事件重新获得权限。

2. REST、Socket.IO 与账号生命周期使用同一授权边界

  • 房间列表、详情、历史、workspace、管理操作和 Socket 加房都经过持久化 policy;未授权用户只得到与不存在房间相同的 Room not found 形状,不能枚举私有房间。
  • 受保护的 REST 路径不把 request-start authentication 当作长期 lease;跨越 await 后重新读取账号状态、角色、Profile entitlement 和房间 grant,再返回数据或执行 mutation。
  • 实时广播按 Socket 逐个重新检查 room.read;读权限被撤销的连接会先退出 Socket.IO room 和 runtime presence,再阻止 message、tool/workspace、stream、reasoning、approval 和 context-status 事件。
  • 同一持久化 subject 的多个 Socket 保留独立连接资格,但成员列表只显示一个成员;只有最后一个 Socket 离开时才产生离开事件。
  • 禁用或删除账号会撤销其 Group Chat actor、membership 和 ownership,并断开该账号的所有相关 Socket。
  • 房间 DTO 分别返回 canManagecanApprovecanLeave;邀请码和 workspace 只对有对应权限的调用方暴露,客户端不再根据 owner/member 状态自行猜测权限。

3. 原子邀请码准入与防枚举保护

  • 邀请加入在一个事务内重新读取房间、邀请码 generation、现有 membership 和 actor;只有当前邀请码或已经授权的 subject 才能原子写入 membership 与 actor grant。
  • 邀请码轮换同时推进 inviteGeneration 和房间授权 revision;旧邀请码与轮换竞态不能留下半写入成员或 actor。
  • 自动邀请码使用 OS / browser 加密随机源,从 32 字符无歧义字母表生成 16 字符代码(80 bits);显式输入的邀请码保持 exact-byte、大小写敏感语义。
  • 邀请码查询使用 domain-separated fixed digest 和 constant-time 比较,不通过 SQL equality 或长短分支形成有效码 / 无效码侧信道。
  • REST 与 Socket.IO 共用有界、fail-closed 的 subject 级失败限流;限流 key 不包含邀请码,达到容量时不会驱逐仍在处罚窗口内的 entry。
  • profile access、room id、错误邀请码和不存在房间都不能成为 room-existence oracle。

4. 服务端权威的加入、复制和退房闭环

  • 客户端补齐输入邀请码加入房间、轮换/复制邀请码和退房操作,并补充中英文文案、OpenAPI、API wrapper 与 Pinia 状态同步。
  • DELETE /api/hermes/group-chat/rooms/:roomId/members/me 会停用当前 authenticated-human actor、移除 membership、驱逐该用户的实时连接并保留房间本身。
  • 房主也可以退房;服务端会同时清除该账号的 ownerAuthUserId,而不是继续保留 owner-only access。退房后,旧 Socket 不能继续发送,且无新邀请码时不能重新进入。
  • 当前选中的房间退房成功后,客户端会清理 room、message、member 和 selection 状态并返回房间列表。
  • 共享 HTTP client 以大小写不敏感方式合并 header,避免 wrapper 自带小写 header 与注入的 JSON、Authorization、Profile 或本地身份 header 形成重复值。

5. Auth-disabled 本地身份与第一方传输一致性

  • Auth-disabled 模式由服务端签发并持久复用 signed local subject;REST、JSON API、二进制 workspace 请求和 Socket.IO 使用同一身份,不再信任客户端显示名、routing id 或伪造的 subject id。
  • 缺失、伪造、有效但未获 grant 或已撤销的本地 credential 都获得零权限;房间列表逐房间重新求值,workspace、invite 和 manage 字段按 capability redact。
  • 本地房间创建和克隆在同一事务内创建 owner actor 与 creator grants,不把权威 subject id 暴露成普通成员 routing/display id。
  • 首方客户端在初始 REST 访问前等待当前 Socket generation 完成本地身份握手;断开或重连会取消旧 generation 的 waiter,避免迟到连接重新发布旧 credential。

6. Agent、审批、Bridge session 与上下文运行时 fencing

  • agent.invoke 同时约束人类触发和 Agent-to-Agent mention;approval.respond 独立于 room.manage,审批事件只投递给当前仍有响应权限的每个 Socket。
  • 每个 Bridge approval id 都绑定来源房间、active agent actor、当前 session fingerprint、允许 choice 集和 permanent-approval 标记;跨房间、重复、冲突、过期、已撤权或未提供的响应全部 fail closed,Python Bridge 再做一次 choice 校验。
  • 外部 Bridge session id 改为基于房间 seed、actor incarnation、authorization/context revision 的固定长度 opaque HMAC,不暴露 room、Profile、workspace、actor 或 capability 原值。
  • Agent 启动、异步模型解析、成员/上下文读取、输入规模估算、压缩锁等待、summary 调用、pre/post launch 与 stream 处理都复查同一 revision lease;撤权或上下文代次变化会停止/interrupt 在途运行,而不是退化为无保护调用。
  • 自动压缩和手动压缩共享每房间 FIFO 锁;summary session 在外部 Bridge 调用前原子登记清理 lease,成功、失败、撤权、Agent 删除和房间删除都进入持久化 session-delete outbox。
  • 清理 worker 按 outbox 中记录的全部 Profile 重放删除,对瞬时失败使用有界指数退避,不依赖当前激活 Profile,也不会因重启遗失待清理 session。
  • 清空上下文、删除房间、替换 workspace 和手动压缩在异步中断/清理之后再次校验管理权限;撤权请求不能继续执行剩余 mutation。

7. Forward-only migration 与 SQLite 进程所有权

  • 新增命名的 groupChatIdentityV1 migration,通过中央 Hermes schema 初始化执行。
  • migration 在一个事务内记录 schema state / minimum reader epoch、回填 canonical actor/capability、轮换无效 legacy room seed,并在成功前验证引用、重复身份和唯一性;pending、foreign、future 或失败状态在任何 schema 写入前 fail closed,注入失败时完整回滚。
  • Hermes Studio SQLite 在 schema 或应用读写前获取进程生命周期 file ownership;第二个进程指向同一数据库时会提前失败,closeDb() 或 owner 进程退出后由内核释放锁并允许重新获取。
  • 新 ownership contract 同步接入现有数据库测试 fixture,避免通过削弱生产锁来让 coverage 通过。

为什么改动范围较大

当前 exact head 为 3d2c43e51ccd2e7540486a1c69cc79af9e210343,相对 base bc0f0b41200ae8be118c39b96460f483b5f112ae 共 96 files / +11,450 -823:其中生产代码 32 files,测试 61 files,文档/OpenAPI 3 files。

这些文件围绕的是同一条授权不变量,而不是互不相关的功能拼盘:

  • 只加 REST guard,仍会从 Socket 广播、workspace、approval 或 Agent runtime 泄露;
  • 只加 Socket guard,history、管理 REST、压缩或 Bridge session 仍可能越权;
  • 不引入 revision lease,跨越 await 的旧授权仍可在撤权后提交;
  • 不迁移 actor/capability,重启后无法保持同一权威身份;
  • 不加数据库进程所有权,migration 和 schema 初始化仍可能被多进程并发破坏;
  • 不更新 DTO、store 与 UI,客户端会继续用房主/成员状态猜测权限并与服务端 policy 漂移;
  • 不覆盖真实 storage、并发 Socket、Bridge、migration、outbox 和 rollback 路径,单元级 happy path 无法证明这条边界闭合。

因此本 PR 保留为一个 independently safe 的 v2 PR1:拆开会产生可被 REST、Socket 或在途运行绕过的中间状态。

升级与回滚

  • groupChatIdentityV1 是 forward-only schema boundary;数据库完成升级后,不支持直接降级到不理解该 reader epoch 的旧版本。
  • 如部署需要保留降级能力,首次启动本版本前必须完整备份 HERMES_WEB_UI_HOME,并将 SQLite 数据库与 .group-chat-local-identity-secret 一起保存;使用 GROUP_CHAT_LOCAL_IDENTITY_SECRET 时必须保留与数据库匹配的同一值。
  • 恢复时需要先停止应用,再成套恢复数据库、Web UI home 和本地身份 secret,并使用兼容该备份的版本启动。
  • Legacy unauthenticated actor 会回填为服务端生成的 subject;已有私有房间的本地客户端可能需要使用当前邀请码重新加入一次以领取 signed credential。已认证账号不受此项影响。

Roadmap 与范围边界

本 PR 是 latest-main Group Chat v2 roadmap 的 PR1 — actor/access foundation。它完成身份、准入、统一授权与 public-room Agent/Bridge 生命周期 fencing,但不宣称后续隐私层已经完成:

  • PR2 — channel / visibility envelope:未包含。 没有 channel、audience、private/agent-only visibility schema、持久化或 UI。
  • PR3 — scoped private-context / durable admission:未包含。 没有 visibility-scoped snapshot/projection、private launch journal 或 non-public Agent dispatch。
  • PR4 — private-fact persistence / grants:未包含。 没有 private-fact schema、写入协议或 actor context grant。
  • 原 roadmap 的 information-transfer protocol:未包含。 没有 handoff、publish、artifact 或 approval-transfer envelope;private-fact self-create/revoke 也不等同于 transfer protocol。
  • Durable event log、orchestrator、统一 Group Chat drivers 和外部平台 adapters 仍属于后续 roadmap。

Related: #2226。该 PR 处理相邻的 mixed-runtime participant 能力;本 PR 不依赖其分支,也不会自动关闭该 issue/PR。

验证

Exact head:3d2c43e51ccd2e7540486a1c69cc79af9e210343

本地:

  • npm run harness:check:通过。
  • npm test -- tests/server/group-chat-invite-admission.test.ts tests/server/group-chat-member-sync.test.ts tests/client/group-chat-panel-workspace-source.test.ts tests/client/group-chat-store-workspace.test.ts tests/client/group-chat-api-invite.test.ts:5 files / 68 tests 通过。
  • npm run test:e2e -- tests/e2e/group-chat-room-deeplink.spec.ts:9 tests 通过。
  • npm test -- tests/server/group-chat-*.test.ts --testTimeout=15000:24 files / 250 tests 通过。
  • npm run build:通过。
  • git diff --check:通过。
  • 最终独立只读复核:VERDICT: NO BLOCKERS,P0/P1/P2 均为空。

GitHub CI(同一 exact head):

  • Build workflow:通过;其中 harness:checktest:coverage 和 production build 全部成功。
  • Playwright workflow:通过。

说明

  • 分支从 origin/main 干净重放,未包含 donor worktree 中无关的 agent-runner 修改。
  • PR 当前为 open、非 draft、mergeable;代码和 CI 已就绪,但尚未经过 maintainer review / merge。

@hanzckernel hanzckernel changed the title feat(group-chat): 强化房间成员权限与邀请操作 feat(group-chat): 建立 v2 actor/access 与运行时撤权基础 Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant